package com.gitee.jdbc.elasticsearch;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.searchbox.action.Action;
import io.searchbox.client.JestClient;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.BulkResult;
import io.searchbox.core.Cat;
import io.searchbox.core.Delete;
import io.searchbox.core.Get;
import io.searchbox.core.Index;
import io.searchbox.core.Search;
import io.searchbox.indices.CreateIndex;
import io.searchbox.indices.DeleteIndex;
import io.searchbox.indices.IndicesExists;
import io.searchbox.indices.mapping.GetMapping;
import io.searchbox.indices.mapping.PutMapping;
import io.searchbox.params.Parameters;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class ElasticsearchClient {

  private static final Logger log = LoggerFactory.getLogger(ElasticsearchClient.class);

  public static final String TYPE_NAME = "_doc";

  private final JestClient jestClient;

  public ElasticsearchClient(JestClient client) {
    this.jestClient = client;
  }

  private <T extends JestResult> T executeInternal(Action<T> request) {
    try {
      return this.jestClient.execute(request);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private Integer getStatus(JestResult rst) {
    JsonObject jsonObject = rst.getJsonObject();
    if (jsonObject.has("status")) {
      return jsonObject.get("status").getAsInt();
    }
    return 600;
  }

  public boolean indicesExists(String indexName) {
    boolean isIndicesExists = false;
    JestResult result = this.executeInternal(new IndicesExists.Builder(indexName).build());
    if (result.isSucceeded()) {
      isIndicesExists = true;
    } else {
      switch (result.getResponseCode()) {
        case 404:
          isIndicesExists = false;
          break;
        case 401:
          // 无权访问
        default:
          throw new RuntimeException(result.getErrorMessage());
      }
    }
    return isIndicesExists;
  }

  public Set<String> indicesNames() {
    Set<String> indices = new HashSet<>();
    JestResult result = this.executeInternal(new Cat.IndicesBuilder().build());
    if (result.isSucceeded()) {
      JsonArray array = result.getJsonObject().getAsJsonArray("result");
      for (JsonElement element : array) {
        indices.add(element.getAsJsonObject().get("index").getAsString());
      }
    } else {
      throw new RuntimeException(result.getErrorMessage());
    }
    return indices;
  }

  public void createIndex(String indexName, Object mappings, String settings, boolean dynamic) {
    JestResult result = null;
    if (!indicesExists(indexName)) {
      log.info("create index " + indexName);
      result = this.executeInternal(
          new CreateIndex.Builder(indexName)
              .settings(settings)
              .setParameter("master_timeout", "5m")
              .build()
      );
      //index_already_exists_exception
      if (!result.isSucceeded()) {
        if (getStatus(result) == 400) {
          log.info(String.format("index [%s] already exists", indexName));
          return;
        } else {
          throw new RuntimeException(result.getErrorMessage());
        }
      } else {
        log.info(String.format("create [%s] index success", indexName));
      }
    }

    int idx = 0;
    while (idx < 5) {
      if (indicesExists(indexName)) {
        break;
      }
      try {
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        log.warn(e.getMessage());
      }
      idx++;
    }
    if (idx >= 5) {
      throw new RuntimeException("Create index '" + indexName + "' failed for 5 times check exists!");
    }

    if (dynamic) {
      log.info("ignore mappings");
      return;
    }
    log.info("create mappings for " + indexName + "  " + mappings);
    result = this.executeInternal(new PutMapping.Builder(indexName, TYPE_NAME, mappings)
        .setParameter("master_timeout", "5m").build());
    if (!result.isSucceeded()) {
      if (getStatus(result) == 400) {
        log.info(String.format("index [%s] mappings already exists", indexName));
      } else {
        throw new RuntimeException(result.getErrorMessage());
      }
    } else {
      log.info(String.format("index [%s] put mappings success", indexName));
    }
  }

  public JsonObject getIndexMappings(String indexName) {
    GetMapping getMapping = new GetMapping.Builder().addIndex(indexName).build();
    JestResult result = this.executeInternal(getMapping);
    if (!result.isSucceeded()) {
      throw new RuntimeException(result.getErrorMessage());
    }
    return result.getJsonObject();
  }

  public List<ESColumn> getIndexProperties(String indexName) {
    GetMapping getMapping = new GetMapping.Builder().addIndex(indexName).build();
    JestResult result = this.executeInternal(getMapping);
    if (!result.isSucceeded()) {
      throw new RuntimeException(result.getErrorMessage());
    }
    List<ESColumn> columns = new ArrayList<>();
    JsonObject jsonObject = result.getJsonObject()
        .getAsJsonObject(indexName)
        .getAsJsonObject("mappings")
        .getAsJsonObject("properties");
    for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
      String propertyName = entry.getKey();
      JsonElement jsonElement = entry.getValue();
      addColumn(null, propertyName, jsonElement, columns);
    }
    return columns;
  }

  private void addColumn(String parent, String propertyName, JsonElement jsonElement, List<ESColumn> columns) {
    String realPropertyName = Objects.nonNull(parent) ? parent + "." + propertyName : propertyName;
    if (null != jsonElement.getAsJsonObject().getAsJsonPrimitive("type")) {
      String typeName = jsonElement.getAsJsonObject().getAsJsonPrimitive("type").getAsString();
      ESColumn column = new ESColumn();
      column.setName(realPropertyName);
      column.setType(ESFieldType.getESFieldType(typeName));
      column.setTimezone(null);
      column.setFormat(null);

      columns.add(column);
    } else if (null != jsonElement.getAsJsonObject().getAsJsonObject("properties")) {
      JsonObject subObject = jsonElement.getAsJsonObject().getAsJsonObject("properties");
      for (Map.Entry<String, JsonElement> subEntry : subObject.entrySet()) {
        String subPropName = subEntry.getKey();
        JsonElement subJsonElement = subEntry.getValue();
        addColumn(realPropertyName, subPropName, subJsonElement, columns);
      }
    }
  }

  public void deleteIndex(String indexName) {
    log.info("delete index " + indexName);
    if (indicesExists(indexName)) {
      JestResult result = this.executeInternal(new DeleteIndex.Builder(indexName).build());
      if (!result.isSucceeded()) {
        throw new RuntimeException(result.getErrorMessage());
      }
    } else {
      log.info("index cannot found, skip delete " + indexName);
    }
  }

  public void insertDocument(String indexName, String id, String source) {
    Index index = new Index.Builder(source).index(indexName).type(TYPE_NAME).id(id).build();
    JestResult result = this.executeInternal(index);
    if (!result.isSucceeded()) {
      throw new RuntimeException(result.getErrorMessage());
    }
  }

  public void bulkInsertDocuments(String indexName, List<String> sources) {
    if (null == sources || sources.size() <= 0) {
      return;
    }
    Bulk.Builder bulk = new Bulk.Builder().defaultIndex(indexName).defaultType(TYPE_NAME);
    for (String source : sources) {
      // 如未设置唯一 id 值，则唯一 id 会默认生成，索引操作为添加操作
      Index index = new Index.Builder(source).build();
      bulk.addAction(index);
    }
    try {
      BulkResult result = jestClient.execute(bulk.build());
      if (!result.isSucceeded()) {
        throw new RuntimeException(result.getErrorMessage());
      }
    } catch (RuntimeException e) {
      throw e;
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public String queryDocument(String indexName, String id) {
    Get get = new Get.Builder(indexName, id).type(TYPE_NAME).build();
    JestResult result = this.executeInternal(get);
    if (!result.isSucceeded()) {
      throw new RuntimeException(result.getErrorMessage());
    }
    return result.getSourceAsString();
  }

  public List<Map> queryAll(String indexName) {
    Search search = new Search.Builder("")
        .setParameter(Parameters.SIZE, 500) //每次获取的数量
        .setParameter(Parameters.SCROLL, "5m") //游标有效时间
        .addIndex(indexName)
        .addType(TYPE_NAME)
        .build();
    JestResult result = this.executeInternal(search);
    if (!result.isSucceeded()) {
      throw new RuntimeException(result.getErrorMessage());
    }
    return result.getSourceAsObjectList(Map.class);
  }

  public void deleteDocument(String indexName, String id) {
    Delete delete = new Delete.Builder(id).index(indexName).type(TYPE_NAME).build();
    JestResult result = this.executeInternal(delete);
    if (!result.isSucceeded()) {
      throw new RuntimeException(result.getErrorMessage());
    }
  }

  public void close() {
    if (this.jestClient != null) {
      this.jestClient.shutdownClient();
    }
  }
}
